package miggy.machine;

import miggy.Debugger;
import miggy.SystemModel;
import miggy.api.config.MachineDefinition;
import miggy.api.cpu.MC680x0;
import miggy.api.cpu.Size;
import miggy.api.display.VideoMode;
import miggy.api.dma.DMAController;
import miggy.api.machine.MachineCore;
import miggy.api.machine.MachineListener;
import miggy.api.machine.MachineState;
import miggy.memory.CustomRegisters;
import miggy.ui.Console;

import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/*
//  Miggy - Java Amiga MachineCore
//  Copyright (c) 2008, Tony Headford
//  All rights reserved.
//
//  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
//  following conditions are met:
//
//    o  Redistributions of source code must retain the above copyright notice, this list of conditions and the
//       following disclaimer.
//    o  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
//       following disclaimer in the documentation and/or other materials provided with the distribution.
//    o  Neither the name of the Miggy Project nor the names of its contributors may be used to endorse or promote
//       products derived from this software without specific prior written permission.
//
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
//  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
//  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
//  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
//  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// $Revision: 21 $
*/
public final class Miggy implements MachineCore, Debugger
{
	private static Logger logger = Logger.getLogger("Miggy");

	private static final int CYCLES_PER_LINE = 455;	//this is 7.xMhz clock cycles not 3.xMhz "colour" cycles
	public static final int PAL_MHZ		= 7093790;	//7.09379 MHz
	public static final int NTSC_MHZ	= 7159090;	//7.15909 MHz
	/** Number of scanlines in the display */
	private int displayLines = 313;
//	private MiggyFrame frame;
//	private DebuggerFrame debugFrame;
	private boolean ntscMode;
	private int cpuSpeed;
	private int vblLines;
	private boolean debugging;
	private long cycleCount;
	private int currentScanline;
	private int lineCycles;

	private VideoMode videoMode;
	private int clockSpeed;

	private ArrayList<MachineListener> listeners;


	private volatile boolean running;
	private volatile boolean handledRequest;
	private volatile MachineState state;
	private volatile int amiga_speed;


	protected final ArrayList<Integer> breakpoints;

	public Miggy()
	{
		state = MachineState.Stopped;
		debugging = false;
		handledRequest = false;
		cycleCount = 0;
		breakpoints = new ArrayList<Integer>();
		ntscMode = false;
		displayLines = 313;
		cpuSpeed = PAL_MHZ;
		currentScanline = 0;
		lineCycles = 0;
		listeners = new ArrayList<MachineListener>();
	}

	public final boolean isDebugging()
	{
		return debugging;
	}

	public void addListener(MachineListener listener)
	{
		if(listeners.contains(listener))
			return;

		listeners.add(listener);
	}

	public void removeListener(MachineListener listener)
	{
		listeners.remove(listener);
	}

	public final void setBreakpoint(int address)
	{
		//autoboxing will happen so wrapping "unnecessary"
		//just making it obvious because autoboxing doesn't work right for overloaded
		//methods ie. remove(int)/remove(Integer) as we're storing Integer objects
		Integer i = new Integer(address);
		if(!breakpoints.contains(i))
		{
			breakpoints.add(i);
		}
	}

	public final void clrBreakpoint(int address)
	{
		//autoboxing will happen so wrapping "unnecessary"
		//just making it obvious because autoboxing doesn't work right for overloaded
		//methods ie. remove(int)/remove(Integer) as we're storing Integer objects
		breakpoints.remove(new Integer(address));
	}

	public final boolean isBreakpointHere(int address)
	{
		//autoboxing will happen so wrapping "unnecessary"
		//just making it obvious because autoboxing doesn't work right for overloaded
		//methods ie. remove(int)/remove(Object) when we're storing Integer objects!
		return breakpoints.contains(new Integer(address));
	}

	private void emulationLoop()
	{
		while(running)
		{
			switch(state)
			{
				case Stopped:
				{
					//wait for a restart from the gui
					logger.fine("Entering stopped state");
					//waits for half a second or an interrupt before returning
					waitForAction();
					break;
				}
				case Running:
				{
					//run emulator
					if(debugging)
					{
						debugEmulation();
					}
					else
					{
						runEmulation();
					}
					break;
				}
				case SingleStep:
				{
					//run one instruction and stop
					singleStep();
					break;
				}
			}
		}
	}

	private synchronized void waitForAction()
	{
		if(handledRequest)
		{
			handledRequest = false;
			//update the debugger to notify that we've finshed request
			//use the Swing thread to do this
//			SwingUtilities.invokeLater(new Runnable() {
//				public void run() { debugFrame.updateData(); }
//			});
		}

		try
		{
			wait(500);
		}
		catch(InterruptedException e)
		{
			logger.fine("Thread interrupted");
		}
	}

	private synchronized void singleStep()
	{
		cycleCount += SystemModel.CPU.execute();
		handledRequest = true;
		changeState(MachineState.Stopped);
	}

	private synchronized void debugEmulation()
	{
		//run the cpu
		MC680x0 cpu = SystemModel.CPU;
		DMAController dma = SystemModel.DMA;

		long start = System.currentTimeMillis();
		int cycles = 0;

		while(running && state == MachineState.Running)
		{
			if(lineCycles >= CYCLES_PER_LINE)
			{
				lineCycles -= CYCLES_PER_LINE;

				currentScanline++;

				if(currentScanline >= displayLines)
				{
					//end of frame - do any vbl tasks
					SystemModel.DISPLAY.endFrame();
					currentScanline = 0;
				}

				//do the dma for this line - disk/audio/sprites/bitplanes/copper/blitter
				lineCycles += dma.execute(currentScanline);

				//manage interrupts
				checkInterrupts();
			}

			int address = cpu.getPC();
			if(isBreakpointHere(address))
			{
				//we've hit a breakpoint so stop
				changeState(MachineState.Stopped);

				//todo: we might need to check that the debugger is visible here
				//otherwise it'll just appear to have hung ?
			}
			else
			{
				lineCycles += cpu.execute();
				cycleCount += lineCycles;
			}
		}
		handledRequest = true;
	}

	private synchronized void runEmulation()
	{
		//run the cpu
		MC680x0 cpu = SystemModel.CPU;
		DMAController dma = SystemModel.DMA;
		
		long start = System.currentTimeMillis();
		long end;
		int cycles = 0;
		int fps= 0;
		int mhz = 7093790;
		int amiga_speed = 0;
		int[] cycle_avg = new int[10];
		int idx = 0;
		int avg = 0;
		int frame_time = 0;

		while(running && state == MachineState.Running)
		{
			//wrap this into the display timing
			//SystemModel.VIDEO.startFrame(); //this would raise a vbl
			frame_time = 0;

			for(currentScanline = 0; currentScanline < displayLines; currentScanline++)
			{
				//do the dma for this line - disk/audio/sprites/bitplanes/copper/blitter
				cycles += dma.execute(currentScanline);

				frame_time += cycles;

				//update timers
				updateTimers(frame_time);

				//manage interrupts
				checkInterrupts();

				while(cycles < CYCLES_PER_LINE)
				{
					cycles += cpu.execute();
				}
				//end of scanline - do any hbl tasks
				cycleCount += cycles;
				frame_time += cycles;
				cycles = cycles - CYCLES_PER_LINE;
			}
			//end of frame - do any vbl tasks
			SystemModel.DISPLAY.endFrame();
			//check for keyboard/mouse/joystick input

			fps++;

			end = System.currentTimeMillis();
			if(end - start > 1000)
			{
				cycle_avg[idx++] = fps;
				if(idx == 10)
				{
					avg = 0;
					for(int n = 0; n < 10; n++)
						avg += cycle_avg[n];

					avg /= 10;
					cycle_avg[0] = avg;
					idx = 1;
				}
//				frame.setTitle("Miggy - " + fps + "(" + avg + ")");
				System.out.println("FPS - " + fps + "(" + avg + ")");
				start = end;
				fps = 0;
			}
		}
		handledRequest = true;
	}

	private void changeState(MachineState state)
	{
		if(this.state != state)
		{
			this.state = state;
			for(MachineListener ml : listeners)
			{
				ml.stateChange(state);
			}
		}
	}

	private void updateTimers(int frameTime)
	{
		//update CIA chips
		SystemModel.CIAA.update(frameTime);
		SystemModel.CIAB.update(frameTime);
	}

	private void checkInterrupts()
	{
		int intena = SystemModel.MEM.peek(CustomRegisters.INTENAR, Size.Word);

		if((intena & 0x4000) == 0)	//master interrupt enable is off
			return;

		int intreq = SystemModel.MEM.peek(CustomRegisters.INTREQR, Size.Word);

		int req = intena & intreq;
		if(req == 0)
			return;

		int flags = (SystemModel.CPU.getSR() & MC680x0.INT_FLAGS) >> 8;
		int request_level = 0;

		if((req & 0x2000) != 0)
		{
			//extern int6 & CIAB
			request_level = 6;
		}
		else if((req & 0x1800) != 0)
		{
			//disk sync matches / serial receive buffer full
			request_level = 5;
		}
		else if((req & 0x0780) != 0)
		{
			//audio channels block finished
			request_level = 4;
		}
		else if((req & 0x0070) != 0)
		{
			//blitter, vert blank, copper
			request_level = 3;
		}
		else if((req & 0x0008) != 0)
		{
			//i/o ports, timers
			request_level = 2;
		}
		else if((req & 0x0007) != 0)
		{
			//software interrupt, disk block finished, serial transmit buffer empty
			request_level = 1;
		}

		if(request_level > flags)
		{
			//trigger an interrupt
			logger.info("raise interrupt " + request_level);
			SystemModel.CPU.raiseInterrupt(request_level);
		}
	}
/*
	private void createAndShowGUI()
	{
		debugFrame = new DebuggerFrame(this, "MiggyMon");
		debugFrame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent we)
			{
				//ensure we're aware that the debugger is hidden
				debug(false);
			}
		});
		//debugFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);

		frame = new MiggyFrame(this, "Miggy");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setVisible(true);
	}
*/
	public final synchronized void start()
	{
		//run the emulator
		if(state != MachineState.Running)
		{
			changeState(MachineState.Running);
			notifyAll();
		}
	}

	public final synchronized void stepInto()
	{
		//execute a single instuction
		if(state != MachineState.SingleStep)
		{
			changeState(MachineState.SingleStep);
			notifyAll();
		}
	}

	public final synchronized void stepOver()
	{
		//todo: this needs sorting
		//execute a single instuction
		if(state != MachineState.SingleStep)
		{
			changeState(MachineState.SingleStep);
			notifyAll();
		}
	}

	public final void stop()
	{
		//stop execution
		changeState(MachineState.Stopped);
	}

	public final void debug(boolean enable)
	{
		if(debugging != enable)
		{
			stop();

//			debugFrame.setVisible(enable);
			debugging = enable;
		}
	}

	public final void reset()
	{
		//reset the emulator
		stop();
		SystemModel.reset();
	}

	public final synchronized void exit()
	{
		//shutdown everything and exit
		for(MachineListener ml : listeners)
		{
			ml.exit();
		}

		running = false;
		notifyAll();
	}

	public MachineState getState()
	{
		return state;
	}

	public final void resetCycles()
	{
		cycleCount = 0;
	}

	public final long getCycles()
	{
		return cycleCount;
	}

	public void boot(MachineDefinition config)
	{
		try
		{
			SystemModel.buildModel(this, config);

			videoMode = SystemModel.CFG.getVideoMode();
			clockSpeed = SystemModel.CFG.getClockSpeed();

			if(videoMode == VideoMode.NTSC)
			{
				displayLines = 263;
				vblLines = 20;
			}
			else
			{
				displayLines = 313;
				vblLines = 25;
			}

			running = true;
/*
			//create the window
			SwingUtilities.invokeLater(new Runnable() {
				public void run() { createAndShowGUI(); }
			});
*/
			Console con = new Console(this);
			con.init();

			//drop into a loop while running, paused for stuff to do
			emulationLoop();
		}
		catch(Exception e)
		{
			e.printStackTrace();
			logger.log(Level.SEVERE, "Unhandled Exception caught", e);
			System.exit(-1);
		}
	}
}
